<?php

namespace yunj\library\snowflake;

use yunj\library\traits\Redis;

class Snowflake {

    use Redis;

    /**
     * 是否可以执行lua脚本
     * @var bool
     */
    private $lua = true;

    // 1bit正负标识位
    const SIGN_BITS = 1;
    // 41bits毫秒级时间戳
    const TIMESTAMP_BITS = 41;
    // 5bits数据中心id
    const DATA_CENTER_BITS = 5;
    // 5bits机器id
    const MACHINE_ID_BITS = 5;
    //12bits毫秒内顺序id
    const SEQUENCE_BITS = 12;

    // 最大数据中心id（范围：0-31）
    private $maxDataCenterId = -1 ^ (-1 << self::DATA_CENTER_BITS);
    // 最大机器id（范围：0-31）
    private $maxMachineId = -1 ^ (-1 << self::MACHINE_ID_BITS);
    // 最大顺序id（范围：0-4095）同一毫秒最多允许生成id数4096个
    private $maxSequenceId = -1 ^ (-1 << self::SEQUENCE_BITS);

    // 标识位左移位数
    private $signLeftShift = self::TIMESTAMP_BITS + self::DATA_CENTER_BITS + self::MACHINE_ID_BITS + self::SEQUENCE_BITS;
    // 毫秒时间戳左移位数
    private $timestampLeftShift = self::DATA_CENTER_BITS + self::MACHINE_ID_BITS + self::SEQUENCE_BITS;
    // 数据中心左移位数
    private $dataCenterLeftShift = self::MACHINE_ID_BITS + self::SEQUENCE_BITS;
    // 机器左移位数
    private $machineLeftShift = self::SEQUENCE_BITS;

    /**
     * 当前数据中心id
     * @var int
     */
    private $dataCenterId = 0;

    /**
     * 当前机器id
     * @var int
     */
    private $machineId = 0;

    /**
     * 开始毫秒时间戳，一旦确定不能改变
     * @var int
     */
    private $epochOffset = 1577836800000;   // 2020-01-01

    /**
     * 上次生成时间
     * @var null|int
     */
    private $lastGenerateIime = null;

    /**
     * @param bool $lua
     * @return Snowflake
     */
    public function setLua(bool $lua = true): Snowflake {
        $this->lua = $lua;
        return $this;
    }

    /**
     * @param int $dataCenterId
     * @return Snowflake
     */
    public function setDataCenterId(int $dataCenterId): Snowflake {
        if ($dataCenterId > $this->maxDataCenterId) throw new \RuntimeException("数据中心id取值范围 0 到 " . $this->maxDataCenterId);
        $this->dataCenterId = $dataCenterId;
        return $this;
    }

    /**
     * @param int $machineId
     * @return Snowflake
     */
    public function setMachineId(int $machineId): Snowflake {
        if ($machineId > $this->maxMachineId) throw new \RuntimeException("机器id取值范围 0 到 " . $this->maxMachineId);
        $this->machineId = $machineId;
        return $this;
    }

    /**
     * @param int $epochOffset
     * @return Snowflake
     */
    public function setEpochOffset(int $epochOffset): Snowflake {
        $this->epochOffset = $epochOffset;
        return $this;
    }

    /**
     * @param int $dataCenterId 数据中心id
     * @param int $machineId 机器id
     * @param int $epochOffset 开始毫秒时间戳，一旦确定不能变更
     */
    public function __construct(?int $dataCenterId = null, ?int $machineId = null, ?int $epochOffset = null) {
        if (!is_null($dataCenterId)) $this->setDataCenterId($dataCenterId);
        if (!is_null($machineId)) $this->setDataCenterId($machineId);
        if (!is_null($epochOffset)) $this->setDataCenterId($epochOffset);
    }

    private function __clone() {
    }

    /**
     * 生成id
     * @return string
     */
    public function nextId(): string {
        $timestamp = $this->getUnixTimestamp();
        if ($timestamp < $this->lastGenerateIime) throw new \RuntimeException("时间发生了回退");
        $sequence = $this->getSequenceId($timestamp);
        if ($sequence > $this->maxSequenceId) {
            // 同一毫秒生成的顺序数超过了允许的最大顺序数，等下一毫秒再生成
            $timestamp = $this->getUnixTimestamp();
            while ($timestamp <= $this->lastGenerateIime) {
                usleep(500);
                $timestamp = $this->getUnixTimestamp();
            }
            $sequence = $this->getSequenceId($timestamp);
        }
        $this->lastGenerateIime = $timestamp;
        $time = (int)($timestamp - $this->epochOffset);
        $id = (0 << $this->signLeftShift) | ($time << $this->timestampLeftShift) | ($this->dataCenterId << $this->dataCenterLeftShift) | ($this->machineId << $this->machineLeftShift) | $sequence;
        return (string)$id;
    }

    /**
     * 解析id
     * @param string $id
     * @return array
     */
    public function parse(string $id): array {
        $binId = decbin($id);   // 转换为2进制
        $len = strlen($binId);
        // 顺序位
        $sequence = substr($binId, $len - self::SEQUENCE_BITS, self::SEQUENCE_BITS);
        // 机器位
        $machineIdStart = $len - self::MACHINE_ID_BITS - self::SEQUENCE_BITS;
        $machineId = substr($binId, $machineIdStart, self::MACHINE_ID_BITS);
        // 数据中心位
        $dataCenterIdStart = $len - self::DATA_CENTER_BITS - self::MACHINE_ID_BITS - self::SEQUENCE_BITS;
        $dataCenterId = substr($binId, $dataCenterIdStart, self::DATA_CENTER_BITS);
        // 毫秒时间戳位
        $timestamp = substr($binId, 0, $dataCenterIdStart);
        $realTimestamp = bindec($timestamp) + $this->epochOffset;   // 真实发生毫秒时间
        return [
            'timestamp' => date('Y-m-d H:i:s', substr($realTimestamp, 0, -3)) . '.' . substr($realTimestamp, -3),
            'dataCenterId' => bindec($dataCenterId),
            'machineId' => bindec($machineId),
            'sequence' => bindec($sequence),
        ];
    }

    /**
     * 获取当前时间顺序id（使用redis来控制并发，可采用lua脚本实现）
     * @param int $timestamp
     * @return int
     */
    private function getSequenceId(int $timestamp): int {
        $redis = $this->getRedis();
        $key = 'yunj.library.snowflake:' . $this->epochOffset . '.' . $this->dataCenterId . '.' . $this->machineId . '.' . $timestamp;
        $ttl = 2;
        if ($this->lua) {
            $lua = <<<LUA
            local sequenceKey = KEYS[1]
            local sequenceTTL = ARGV[1]
            if redis.call('set', sequenceKey, 0, "EX", sequenceTTL, "NX") then
                return 0
            else
                return redis.call('incr', sequenceKey)
            end
LUA;
            $sequenceId = $redis->eval($lua, [$key, $ttl], 1);
            $luaError = $redis->getLastError();
            if (isset($luaError)) {
                throw new \ErrorException($luaError);
            }
            return $sequenceId;
        }
        // incr 如果 key 不存在，那么 key 的值会先被初始化为 0 ，然后再执行 +1 操作。
        $sequenceId = $redis->incr($key) - 1;
        $redis->expire($key, $ttl);
        return $sequenceId;
    }

    /**
     * 获取当前毫秒时间戳
     * @return int
     */
    private function getUnixTimestamp(): int {
        return floor(microtime(true) * 1000);
    }


}